﻿using System;
using System.Diagnostics;
using System.IO;
using System.Threading;

namespace ProcessUtilities
{
    /// <summary>
    /// This is a process gong. It runs a test of some sort waiting for a a condition to not 
    /// happen for some amount of time. If it doesn't happen (i.e. a file update) then it will
    /// kill the process.
    /// </summary>
    public class ProcessGong
    {
        /// <summary>
        /// Common code to watch for the modified date of a file to change.
        /// </summary>
        public class WatchForSingleFileChange : IWatchForChange
        {
            private FileInfo _fileInfo;

            /// <summary>
            /// Create a watcher for the fileInfo file.
            /// </summary>
            /// <param name="fileInfo"></param>
            public WatchForSingleFileChange(FileInfo fileInfo)
            {
                this._fileInfo = fileInfo;
            }

            /// <summary>
            /// True if the file has changed!
            /// </summary>
            /// <param name="lastChange"></param>
            /// <returns></returns>
            public bool HasChanged(DateTime lastChange)
            {
                _fileInfo.Refresh();
                return _fileInfo.LastWriteTime > lastChange;
            }
        }

        /// <summary>
        /// Implement this interface so that you can watch for things to change.
        /// </summary>
        public interface IWatchForChange
        {
            /// <summary>
            /// Return true if the file has changed since the date/time of last change.
            /// </summary>
            /// <param name="lastChange"></param>
            /// <returns></returns>
            bool HasChanged(DateTime lastChange);
        }

        /// <summary>
        /// Process name to be killed if the timeout is hit.
        /// </summary>
        private string _procName;

        /// <summary>
        /// How long for no change to occur before we kill off the process.
        /// </summary>
        private int _timeOut;

        /// <summary>
        /// True if we should stop the gong (if it is going).
        /// </summary>
        private bool _stopRunning;

        /// <summary>
        /// Creates a new process gong.
        /// </summary>
        /// <param name="procName">Name of process to kill that can be looked up in GetProcessByName - must be unique!</param>
        /// <param name="timeOut">How long for no change to happen before we kill things off (msec)</param>
        public ProcessGong(string procName, int timeOut)
        {
            this._procName = procName;
            this._timeOut = timeOut;

            Fired = false;
            CheckDelay = 25;
        }

        /// <summary>
        /// The object used to wait for a watcher to go off.
        /// </summary>
        public IWatchForChange Watcher { get; set; }

        /// <summary>
        /// Start the timeer waiting for the timeout to occur.
        /// </summary>
        public void StartWait()
        {
            if (Watcher == null)
            {
                throw new InvalidOperationException("The Watcher isn't set on the ProcessGong - can't start a wait");
            }

            ///
            /// Rest the various variables
            /// 

            Fired = false;
            _stopRunning = false;

            ///
            /// Start the background watcher thing-y.
            /// 

            ThreadPool.QueueUserWorkItem(val => WatchForChanges(Watcher));
        }

        /// <summary>
        /// Do the actual waiting!
        /// </summary>
        /// <param name="Watcher"></param>
        /// <returns></returns>
        private void WatchForChanges(IWatchForChange watcher)
        {
            DateTime lastChange = DateTime.Now;
            while (!_stopRunning)
            {
                Thread.Sleep(CheckDelay);
                if (watcher.HasChanged(lastChange))
                {
                    lastChange = DateTime.Now;
                }
                DateTime nextChange = DateTime.Now;
                TimeSpan delta = nextChange - lastChange;
                if (delta.TotalMilliseconds > _timeOut)
                {
                    KillProcess();
                    break;
                }
            }
        }

        /// <summary>
        /// Kill off the process, if it is there.
        /// </summary>
        private void KillProcess()
        {
            Fired = true;
            int count = 0;
            while (true)
            {
                var procs = Process.GetProcessesByName(_procName);
                if (procs.Length == 0)
                {
                    return;
                }

                if (procs.Length != 1)
                {
                    count++;
                    if (count == 120)
                    {
                        throw new InvalidOperationException("There is more than one process of name " + _procName + " running right now! Don't know what to kill!");
                    }
                    Thread.Sleep(1 * 60 * 60 * 1000);
                }

                if (procs.Length == 1)
                {
                    try
                    {
                        procs[0].Kill();
                    }
                    catch { }
                    return;
                }
            }
        }

        /// <summary>
        /// Stop a wait that is in progress. Ok to stop if there is no wait running right now!
        /// </summary>
        public void StopWait()
        {
            _stopRunning = true;
        }

        /// <summary>
        /// This is true if the gong has been fired. Only valid after issuing a StartWait.
        /// </summary>
        public bool Fired { get; private set; }

        /// <summary>
        /// Get/Set # of miliseconds to wait between checks. Defaults to 25.
        /// </summary>
        public int CheckDelay { get; set; }
    }
}
